﻿using NanoMvvm.Async;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace NanoMvvm
{
    public class AsyncTaskViewModel : PropertyObservableBase
    {
        private readonly ReaderWriterLockSlim runLock;

        private readonly Action<CancellationToken> tokenAction;

        private readonly Action action;

        private readonly Timer runningTimer;
        private CancellationTokenSource cancelSource;
        private Task theTask;
        private bool isStarted;

        private bool isRunning;

        private bool cancelRequested;

        private int periodDuration;

        private bool startPeriodElapsed;

        private bool loopPeriodTimer;

        private bool isCompleted;

        private bool isCancelled;

        private bool isFaulted;

        private double progress;

        private bool isFinished;

        public AsyncTaskViewModel(Action action)
        {
            if (action == null)
            {
                throw new ArgumentNullException("action");
            }

            runLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
            
            this.action = action;

            CreateTask();

            runningTimer = new Timer(OnPeriodElapsed);
        }

        public AsyncTaskViewModel(Action<CancellationToken> tokenAction)
        {
            if (tokenAction == null)
            {
                throw new ArgumentNullException("tokenAction");
            }

            runLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

            this.tokenAction = tokenAction;

            CreateTask();

            runningTimer = new Timer(OnPeriodElapsed);
        }

        public event EventHandler Started;

        public event EventHandler Completed;

        public event EventHandler Cancelled;

        public event EventHandler Finished;

        public event EventHandler<ExceptionEventArgs> Faulted;

        public event EventHandler PeriodElapsed;

        public bool IsStarted
        {
            get
            {
                return isStarted;
            }

            private set
            {
                SetProperty(ref isStarted, value, () => IsStarted);
            }
        }

        public bool IsRunning
        {
            get
            {
                runLock.EnterReadLock();
                var value = isRunning;
                runLock.ExitReadLock();
                return value;
            }

            private set
            {
                runLock.EnterWriteLock();
                SetProperty(ref isRunning, value, () => IsRunning);
                runLock.ExitWriteLock();
            }
        }

        public bool IsCompleted
        {
            get
            {
                return isCompleted;
            }

            private set
            {
                SetProperty(ref isCompleted, value, () => IsCompleted);
            }
        }

        public bool IsCancelled
        {
            get
            {
                return isCancelled;
            }

            private set
            {
                SetProperty(ref isCancelled, value, () => IsCancelled);
            }
        }

        public bool IsFaulted
        {
            get
            {
                return isFaulted;
            }

            private set
            {
                SetProperty(ref isFaulted, value, () => IsFaulted);
            }
        }

        public bool IsFinished
        {
            get
            {
                return isFinished;
            }

            set
            {
                SetProperty(ref isFinished, value, () => IsFinished);
            }
        }

        public bool CancelRequested
        {
            get
            {
                return cancelRequested;
            }

            private set
            {
                SetProperty(ref cancelRequested, value, () => CancelRequested);
            }
        }

        public int PeriodDuration
        {
            get
            {
                return periodDuration;
            }

            set
            {
                SetProperty(ref periodDuration, value, () => PeriodDuration);
            }
        }

        public bool StartPeriodElapsed
        {
            get
            {
                return startPeriodElapsed;
            }

            private set
            {
                SetProperty(ref startPeriodElapsed, value, () => StartPeriodElapsed);
            }
        }

        public bool LoopPeriodTimer
        {
            get
            {
                return loopPeriodTimer;
            }

            set
            {
                SetProperty(ref loopPeriodTimer, value, () => LoopPeriodTimer);
            }
        }

        public double Progress
        {
            get
            {
                return progress;
            }

            internal set
            {
                SetProperty(ref progress, value, () => Progress);
            }
        }

        public void Start()
        {
            if (IsFinished || IsRunning)
            {
                return;
            }

            try
            {
                theTask.Start();
                OnStarted();
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Exception: " + ex.Message);
            }
        }

        public void Reset()
        {
            if (!IsRunning)
            {
                IsFinished = false;
                IsCancelled = false;
                IsCompleted = false;
                IsFaulted = false;
                IsStarted = false;
                runningTimer.Change(Timeout.Infinite, Timeout.Infinite);

                CreateTask();
            }
            else
            {
                EventHandler a = null;
                a = delegate
                {
                    Reset();
                    Finished -= a;
                };

                Finished += a;
            }
        }

        public void RequestCancel()
        {
            if (!CancelRequested)
            {
                cancelSource.Cancel();
                CancelRequested = true;
            }
        }

        private void CreateTask()
        {
            cancelSource = new CancellationTokenSource();

            if (action != null)
            {
                theTask = new Task(action);
            }
            else if (tokenAction != null)
            {
                theTask = new Task(arg => tokenAction((CancellationToken)arg), cancelSource.Token, cancelSource.Token);
            }
            else
            {
                throw new InvalidOperationException();
            }

            AttachContinuations(theTask);
        }

        private void AttachContinuations(Task task)
        {
            if (task == null)
            {
                throw new ArgumentNullException("task");
            }

            task.ContinueWith(_ => OnCompleted(), TaskContinuationOptions.OnlyOnRanToCompletion)
                .ContinueWith(_ => OnCancelled(), TaskContinuationOptions.OnlyOnCanceled)
                .ContinueWith(OnFaulted, TaskContinuationOptions.OnlyOnFaulted)
                .ContinueWith(_ => OnFinished());
        }

        private void OnStarted()
        {
            IsStarted = true;
            IsRunning = true;

            if (PeriodDuration > 0)
            {
                runningTimer.Change(periodDuration, LoopPeriodTimer ? periodDuration : Timeout.Infinite);
            }

            var handler = Started;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }

        private void OnCompleted()
        {
            IsRunning = false;
            IsCompleted = true;

            runningTimer.Change(Timeout.Infinite, Timeout.Infinite);

            var handler = Completed;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }

        private void OnCancelled()
        {
            IsRunning = false;
            IsCancelled = true;

            runningTimer.Change(Timeout.Infinite, Timeout.Infinite);

            var handler = Cancelled;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }

        private void OnFaulted(Task t)
        {
            IsRunning = false;
            IsFaulted = true;

            runningTimer.Change(Timeout.Infinite, Timeout.Infinite);

            var handler = Faulted;
            if (handler != null)
            {
                try
                {
                    t.Wait();
                }
                catch (AggregateException e)
                {
                    Debug.WriteLine("Task exception: " + e.Flatten().InnerException.Message);
                    handler(this, new ExceptionEventArgs(e.Flatten().InnerException));
                }
            }
        }

        private void OnFinished()
        {
            IsRunning = false;
            IsFinished = true;

            runningTimer.Change(Timeout.Infinite, Timeout.Infinite);

            var handler = Finished;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }

        private void OnPeriodElapsed(object state)
        {
            if (StartPeriodElapsed && !loopPeriodTimer)
            {
                runningTimer.Change(Timeout.Infinite, Timeout.Infinite);
            }

            StartPeriodElapsed = true;

            var handler = PeriodElapsed;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }
    }
}